Thick End of the Wedg

Simulating Monopoly¶

This is a simulation of movements around the board in the game of Monopoly. Ultimately we'll produce a table of frequencies of the squares that you could expect to land on.

The simulation takes into account die rolls, cards (chance and community chest), and going to jail.

Going to jail can happen in any of three different ways:
- landing on the "go to jail" square,
- receiving a "go to jail" card, or
- rolling doubles three times in a row.

The one weak assumption is regarding time spent in jail. When in jail players can exit in one of three ways:
- using a "get out of jail card",
- paying a fine before their next role, or
- rolling a double and moving by that number.

This simulation assumes that a player will exit straight away i.e. paying the fine straight away and moving on their next turn. In practice it's better to exit straight away early in the game (when you're trying to buy property) but to remain in jail towards the end of the game (when you're trying to avoid opponents' properties but still earn "rent" from your opponents landing on your properties).

We're also going to explore the difference betwee running one simulation many times - this should approach the long-run probabilities derived by a Markov chain - vs running many simulations a small number of times which should more accurately reflect the actual probabilities given that each player will have around 30 turns in total.

Packages¶

Dataframes is used to store the results in a table and Gadfly is used for the plots.

In [1]:
using DataFrames, Gadfly

1. Set up¶

Board and cards¶

Board¶

There are 40 "squares" on the board. The naming is as follows:

  • The 22 properties are arranged in 8 property groups beginning with the letters "A" to "H".
  • The 4 railway stations begin with "R".
  • The 2 utilities begin with "U".
  • The 2 taxation squares begin with "T".
  • The 3 chance card squares begin with "CH".
  • The 3 community chest cards begfin with "CC".
  • The 1 beginning square is called "GO".
  • The 1 jail square is called "JAIL".
  • The 1 free parking square is called "FP".
  • The 1 "Go to Jail" square is called "G2J".

Community Chest Cards¶

See list here. There are 16 cards in total and 2 cards that influence board position:

  1. Advance to Go
  2. Go to Jail

Chance Cards¶

See list here. There are 16 cards in total and 10 cards that influence board position:

  1. Advance to Go
  2. Go to Jail
  3. Advance to C1 - Pall Mall (UK) / St. Charles Place (US)
  4. Advance to E3 - Trafalgar Square (UK) / Illinois Ave. (US)
  5. Advance to H2 - Mayfair (UK) / Boardwalk (US)
  6. Take a trip to R1 - Marylebone Station (UK) / Reading Railroad (US)
  7. Advance to the nearest Railroad.
  8. Advance to the nearest Railroad. (I.e. there are two of these cards)
  9. Advance to the nearest Utility.
  10. Go Back 3 Spaces
In [2]:
# The board: a list of the names of the 40 squares
board = split("GO   A1 CC1 A2  T1 R1 B1  CH1 B2 B3 
               JAIL C1 U1  C2  C3 R2 D1  CC2 D2 D3 
               FP   E1 CH2 E2  E3 R3 F1  F2  U2 F3 
               G2J  G1 G2  CC3 G3 R4 CH3 H1  T2 H2")

# The deck of 16 community chest cards.
CC = ["GO", "JAIL", fill("£", 14)...]

# The deck of 16 chance cards.
CH = ["GO", "JAIL", "C1", "E3", "H2", "R1", "R", "R", "U", "-3", fill("£", 6)...];

Colours¶

The frequency chart will look better if the board colours are recognisable. This boardcolours function maps the board squares to colours.

In [3]:
function boardcolours(str)
    if     str == "GO"      return colorant"khaki"
    elseif str == "JAIL"    return colorant"khaki"
    elseif str == "FP"      return colorant"khaki"
    elseif str == "G2J"     return colorant"khaki"
    elseif str[1:2] == "CC" return colorant"grey50"
    elseif str[1:2] == "CH" return colorant"grey50"
    elseif str[1] == 'R'    return colorant"lightgrey"
    elseif str[1] == 'T'    return colorant"black"
    elseif str[1] == 'U'    return colorant"lightgrey"
    elseif str[1] == 'A'    return colorant"saddlebrown"
    elseif str[1] == 'B'    return colorant"lightblue"
    elseif str[1] == 'C'    return colorant"deeppink"
    elseif str[1] == 'D'    return colorant"orange"
    elseif str[1] == 'E'    return colorant"red"
    elseif str[1] == 'F'    return colorant"yellow"
    elseif str[1] == 'G'    return colorant"green"
    elseif str[1] == 'H'    return colorant"blue"
    else                    return colorant"black"
    end
end;

Street and place names - UK and USA¶

In [4]:
uk_names = [
    "Go", "Old Kent Road", "Community Chest 1", "Whitechapel Road", "Income Tax",
    "Kings Cross Station", "The Angel Islington", "Chance 1", "Euston Road", "Pentonville Road",
    "Jail", "Pall Mall", "Electric Company", "Whitehall", "Northumberland Avenue",
    "Marylebone Station", "Bow Street", "Community Chest 2", "Marlborough Street", "Vine Street",
    "Free Parking", "Strand", "Chance 2", "Fleet Street", "Trafalgar Square",
    "Fenchurch Street Station", "Leicester Square", "Coventry Street", "Water Works", "Piccadilly",
    "Go to Jail", "Regent Street", "Oxford Street", "Community Chest 3", "Bond Street",
    "Liverpool Street Station", "Chance 3", "Park Lane", "Super Tax", "Mayfair"]

us_names = [
    "Go", "Mediterranean Avenue", "Community Chest 1", "Baltic Avenue", "Income Tax",
    "Reading Railroad", "Oriental Avenue", "Chance 1", "Vermont Avenue", "Connecticut Avenue",
    "Jail", "St. Charles Place", "Electric Company", "States Avenue", "Virginia Avenue",
    "Pennsylvania Railroad", "St James Place", "Community Chest 2", "Tennessee Avenue", "New York Avenue",
    "Free Parking", "Kentucky Avenue", "Chance 2", "Indiana Avenue", "Illinois Avenue",
    "B&O Railroad", "Atlantic Avenue", "Ventnor Avenue", "Water Works", "Marvin Gardens",
    "Go to Jail", "Pacific Avenue", "No. Carolina Avenue", "Community Chest 3", "Pennsylvania Avenue",
    "Short Line Railroad", "Chance 3", "Park Place", "Super Tax", "Boardwalk"];

2. Simulation of one long game¶

Moving¶

Moving is done by either:

  • going to the number of the board square - e.g. if token is on square number 10 and a 7 is rolled the token is advanced to suare number 17, or
  • going to a named square.
In [5]:
# Go to destination square, which can be either a number (board position) or a name. Update 'here'.
goto(square::Int, board=board) = mod1(square, length(board))     # Modulo, as can keep going round
goto(square::String, board=board) = findfirst(board, square)     # Board location of named square;

Selecting a card¶

Card is selected from the top of the deck and replaced at the bottom. There are 3 types of movement cards:

  1. Go to the next railroad or utility - in which case we step until that is reached
  2. Go back 3 spaces
  3. Go to a named square
In [6]:
# Take the top card from deck and do what it says.
function take_card(here, deck, board=board)
    card = pop!(deck)                           # Lift card from top of deck
    unshift!(deck, card)                        # Put card back on bottom of deck
    if card == "R" || card == "U"               # Advance to next railroad or utility 
        while string(board[here][1]) != card    # Keep stepping until you get there
            here = goto(here + 1)
        end
    elseif card == "-3"                         # Go back 3 spaces
        here = goto(here - 3)
    elseif card == "£"                          # Card is about money, not about movement
        nothing
    else                                        # Go to destination named on card
        here = goto(card)
    end
    here
end;

Simulating a game¶

In [7]:
# Simulate given number of steps of monopoly game, yielding the name of the current square after each step.
function monopoly(steps, board=board, CC=CC, CH=CH)

    # Prep for game
    results = zeros(Int, length(board))    # Initialise zero vector - used for counts
    shuffle!(CC)                           # Shuffle the Community Chest pack
    shuffle!(CH)                           # Shuffle the Chance pack
    here = 1                               # Start at position 1 i.e. "GO"
    doubles = 0                            # Initialise counter of doubles - 3 doubles in a row -> "GO TO JAIL"

    # Play game
    for i = 1:steps
        d1, d2 = rand(1:6), rand(1:6)      # Two dice rolls - uniformly selected from integers 1 to 6
        here = goto(here + d1 + d2) # Go to new position i.e. current position + dice roll
        d1 == d2 ? doubles = (doubles + 1) : doubles = 0    # If double is rolled add to double counter
        if doubles == 3 || board[here] == "G2J"             # 3 doubles in a row or land on "GO TO JAIL"
            here = goto("JAIL")
        elseif board[here][1:2] == "CC"    # If land on Community Chest take a card and move if needed
            here = take_card(here, CC)
        elseif board[here][1:2] == "CH"    # If land on Chance take a card and move if needed
            here = take_card(here, CH)
        end
        results[here] += 1                 # Add to results for the updated position
    end

    # Return the tallies of each board square
    return results
end;

Run single game¶

The first simulation plays 1 million turns of the game defined above. The output frequencies should be the same as the long run probabilities calculated by a Markov chain. This website has calculated those probabilities and even includes a full transition matrix. I'll compare below the probabilities derived here from this single game simulation with the probabilites derived by the Markov chain. The only difference with the game setup here vs that page is that here there is no distinction between "just visiting jail" and "in jail".

In [9]:
monopoly(100) # Warm up

# The output vector is a fixed size - i.e. does not depend on the number of simulations - so we can run quite a big number. 
# Julia is great at this.
@time results = monopoly(10_000_000);
 14.928529 seconds (58.36 M allocations: 1.442 GiB, 1.34% gc time)

Store results in a DataFrame¶

In [10]:
df = DataFrame(board = board, uk_names = uk_names, us_names = us_names, boardcolour = boardcolours.(board),
               sim1_prob = results ./ sum(results) * 100);

Plot probabilities¶

I think, in this case, the vertical bars are easier to read than the horizontal.

1) Vertical bars with UK names:

In [11]:
set_default_plot_size(25cm, 25cm/golden)
plot(df, x=:uk_names, y=:sim1_prob, color=:boardcolour, Geom.bar,
     Guide.xlabel(nothing), Guide.ylabel("Frequency"), Guide.yticks(ticks=[0:7;]),
     Scale.y_continuous(labels=y -> @sprintf("%0.2f%%", y)),
     Theme(bar_spacing=0.7mm))
Out[11]:
Go Old Kent Road Community Chest 1 Whitechapel Road Income Tax Kings Cross Station The Angel Islington Chance 1 Euston Road Pentonville Road Jail Pall Mall Electric Company Whitehall Northumberland Avenue Marylebone Station Bow Street Community Chest 2 Marlborough Street Vine Street Free Parking Strand Chance 2 Fleet Street Trafalgar Square Fenchurch Street Station Leicester Square Coventry Street Water Works Piccadilly Go to Jail Regent Street Oxford Street Community Chest 3 Bond Street Liverpool Street Station Chance 3 Park Lane Super Tax Mayfair 0.00% 1.00% 2.00% 3.00% 4.00% 5.00% 6.00% 7.00% Frequency

2) Horizontal bars with US names:

In [12]:
set_default_plot_size(25cm, 20cm)
plot(df, x=:sim1_prob, y=:us_names, color=:boardcolour, Geom.bar(orientation=:horizontal),
     Guide.ylabel(nothing), Guide.xlabel("Frequency"), Guide.xticks(ticks=[0:7;]),
     Scale.x_continuous(labels=x -> @sprintf("%0.2f%%", x)),
     Scale.y_discrete(order=collect(40:-1:1)),
     Theme(bar_spacing = 0.5mm))
Out[12]:
Frequency 0.00% 1.00% 2.00% 3.00% 4.00% 5.00% 6.00% 7.00% Boardwalk Super Tax Park Place Chance 3 Short Line Railroad Pennsylvania Avenue Community Chest 3 No. Carolina Avenue Pacific Avenue Go to Jail Marvin Gardens Water Works Ventnor Avenue Atlantic Avenue B&O Railroad Illinois Avenue Indiana Avenue Chance 2 Kentucky Avenue Free Parking New York Avenue Tennessee Avenue Community Chest 2 St James Place Pennsylvania Railroad Virginia Avenue States Avenue Electric Company St. Charles Place Jail Connecticut Avenue Vermont Avenue Chance 1 Oriental Avenue Reading Railroad Income Tax Baltic Avenue Community Chest 1 Mediterranean Avenue Go

Compare simulation probabilities with Markov chain probabilities¶

How close are the probabilities calculated from the simulation to the probabilities calculated by the transition matrix? The plot below shows they are a very close match - all are a match to the first decimal point.

In [13]:
# Markov chain probabilities copied from http://www.tkcs-collins.com/truman/monopoly/monopoly.shtml
df[:mc_prob] = [3.0961,2.1314,1.8849,2.1624,2.3285,2.9631,2.2621,0.865,2.321,2.3003,6.2194,2.7017,2.604,2.3721,2.4649,2.92,2.7924,2.5945,2.9356,3.0852,2.8836,2.8358,1.048,2.7357,3.1858,3.0659,2.7072,2.6789,2.8074,2.5861,0,2.6774,2.6252,2.3661,2.5006,2.4326,0.8669,2.1864,2.1799,2.626]
df[:sim_vs_mc] = df[:sim1_prob] .- df[:mc_prob];
In [14]:
set_default_plot_size(25cm, 25cm/golden)
plot(sort(df, cols=:sim_vs_mc, rev=true), x=:uk_names, y=:sim_vs_mc, #color=:boardcolour, 
     Geom.bar, Guide.xlabel(nothing), Guide.ylabel("Simulation vs Markov Chain", orientation=:vertical),
     Guide.yticks(ticks=[-0.05:0.01:0.05;]), Scale.y_continuous(labels=y -> @sprintf("%0.2f ppt", y)), Theme(bar_spacing=1mm))
Out[14]:
Water Works Community Chest 3 Jail Regent Street Fenchurch Street Station Chance 2 Park Lane Mayfair Euston Road The Angel Islington Super Tax Old Kent Road Pentonville Road Marylebone Station Strand Liverpool Street Station Vine Street Whitechapel Road Bond Street Chance 3 Piccadilly Whitehall Community Chest 1 Go to Jail Oxford Street Community Chest 2 Income Tax Coventry Street Kings Cross Station Pall Mall Leicester Square Bow Street Trafalgar Square Fleet Street Chance 1 Marlborough Street Go Northumberland Avenue Free Parking Electric Company -0.05 ppt -0.04 ppt -0.03 ppt -0.02 ppt -0.01 ppt 0.00 ppt 0.01 ppt 0.02 ppt 0.03 ppt 0.04 ppt 0.05 ppt Simulation vs Markov Chain

3. Play multiple games¶

Let's find out how different the probabilities will be if, instead of playing one long game (1 million turns in simulation 1 above) we play many games starting again at "Go". The idea being that if a full game takes only - say - 30 turns the probabilities of landing on each square will be different to the long run Markov chain probabilities.

This is really for interest as I don't think these probabilities are as relevant for Monopoly strategies. What is probably most important is - towards the end of the game, when everyone's pieces are randomly spread around the board, what is the probability of you landing on someone elses hotels and what is the probability of everyone else landing on yours.

First we need a function to collect the results of many games.

In [15]:
function monopolies(games, steps, board=board, CC=CC, CH=CH)

    # Prep for games
    results = zeros(Int, length(board))    # Initialise zero vector - used for counts

    # Play games
    for i = 1:games
        results .+= monopoly(steps)
    end

    return results
end;

Run the simulation¶

In [16]:
monopolies(100, 30) # Warm up

# Sim 2
@time results = monopolies(400_000, 30);
 19.737446 seconds (48.20 M allocations: 1.580 GiB, 1.93% gc time)

Store and plot the results¶

In [17]:
df[:sim2_prob] = results ./ sum(results) * 100
plot(df, x=:uk_names, y=:sim2_prob, color=:boardcolour, Geom.bar,
     Guide.xlabel(nothing), Guide.ylabel("Frequency"),
     Scale.y_continuous(labels=x -> @sprintf("%0.2f%%", x)),
     Theme(bar_spacing = 1mm))
Out[17]:
Go Old Kent Road Community Chest 1 Whitechapel Road Income Tax Kings Cross Station The Angel Islington Chance 1 Euston Road Pentonville Road Jail Pall Mall Electric Company Whitehall Northumberland Avenue Marylebone Station Bow Street Community Chest 2 Marlborough Street Vine Street Free Parking Strand Chance 2 Fleet Street Trafalgar Square Fenchurch Street Station Leicester Square Coventry Street Water Works Piccadilly Go to Jail Regent Street Oxford Street Community Chest 3 Bond Street Liverpool Street Station Chance 3 Park Lane Super Tax Mayfair -10.00% -8.00% -6.00% -4.00% -2.00% 0.00% 2.00% 4.00% 6.00% 8.00% 10.00% 12.00% 14.00% 16.00% 18.00% -8.00% -7.50% -7.00% -6.50% -6.00% -5.50% -5.00% -4.50% -4.00% -3.50% -3.00% -2.50% -2.00% -1.50% -1.00% -0.50% 0.00% 0.50% 1.00% 1.50% 2.00% 2.50% 3.00% 3.50% 4.00% 4.50% 5.00% 5.50% 6.00% 6.50% 7.00% 7.50% 8.00% 8.50% 9.00% 9.50% 10.00% 10.50% 11.00% 11.50% 12.00% 12.50% 13.00% 13.50% 14.00% 14.50% 15.00% 15.50% 16.00% -10.00% 0.00% 10.00% 20.00% -8.00% -7.50% -7.00% -6.50% -6.00% -5.50% -5.00% -4.50% -4.00% -3.50% -3.00% -2.50% -2.00% -1.50% -1.00% -0.50% 0.00% 0.50% 1.00% 1.50% 2.00% 2.50% 3.00% 3.50% 4.00% 4.50% 5.00% 5.50% 6.00% 6.50% 7.00% 7.50% 8.00% 8.50% 9.00% 9.50% 10.00% 10.50% 11.00% 11.50% 12.00% 12.50% 13.00% 13.50% 14.00% 14.50% 15.00% 15.50% 16.00% Frequency

Compare these probabilities with the first simulation¶

There isn't a material difference. And the small differences are intuitive - e.g. it is now less likely to land on the first square after Go i.e. Old Kent Road and more likely to land on the squares within one roll of Go (e.g. the light blue streets Euston Road, Angel Islington and Pentonville Road).

In [18]:
df[:sim2_vs_sim1] = df[:sim2_prob] .- df[:sim1_prob];
plot(sort(df, cols=:sim2_vs_sim1, rev=true), x=:uk_names, y=:sim2_vs_sim1, #color=:boardcolour, 
     Geom.bar, Guide.xlabel(nothing), Guide.ylabel("Simulation 2 vs Simulation 1", orientation=:vertical),
     Guide.yticks(ticks=[-0.4:0.1:0.4;]), Scale.y_continuous(labels=y -> @sprintf("%0.2f ppt", y)),
     Theme(bar_spacing=1mm))
Out[18]:
Euston Road The Angel Islington Pentonville Road Pall Mall Electric Company Chance 1 Kings Cross Station Marylebone Station Income Tax Northumberland Avenue Bow Street Jail Whitehall Community Chest 2 Marlborough Street Vine Street Free Parking Strand Go to Jail Chance 2 Fleet Street Trafalgar Square Whitechapel Road Fenchurch Street Station Leicester Square Chance 3 Coventry Street Piccadilly Community Chest 1 Water Works Community Chest 3 Regent Street Oxford Street Liverpool Street Station Bond Street Park Lane Super Tax Mayfair Go Old Kent Road -0.40 ppt -0.30 ppt -0.20 ppt -0.10 ppt 0.00 ppt 0.10 ppt 0.20 ppt 0.30 ppt 0.40 ppt Simulation 2 vs Simulation 1
In [ ]: